package net.aiion.weave.impl.injection.transformation;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;

import net.aiion.weave.asm.Label;
import net.aiion.weave.asm.Opcodes;
import net.aiion.weave.asm.Type;
import net.aiion.weave.spi.bytecode.Bytecode;
import net.aiion.weave.spi.bytecode.BytecodeContext;
import net.aiion.weave.spi.bytecode.BytecodeException;
import net.aiion.weave.spi.bytecode.BytecodeGenerator;
import net.aiion.weave.spi.bytecode.events.AnnotationStartBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.BytecodeEvent;
import net.aiion.weave.spi.bytecode.events.ClassEndBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.ClassStartBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.FieldEndBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.FieldStartBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.MethodEndBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.MethodStartBytecodeEvent;
import net.aiion.weave.spi.bytecode.util.BytecodeTransformation;
import net.aiion.weave.spi.injection.InjectionException;
import net.aiion.weave.spi.injection.ContextualFactory;
import net.aiion.weave.spi.injection.InjectionService;
import net.aiion.weave.spi.injection.annotations.Inject;
import net.aiion.weave.spi.naming.Context;
import net.aiion.weave.spi.naming.Contextual;
import net.aiion.weave.spi.naming.annotations.DynamicScoped;

public class InjectionTransformation extends BytecodeTransformation
{
    private BytecodeGenerator contextualFactoryBytecodeGenerator;

    private Collection<BytecodeEvent> createContextualBytecodeEvents;
    private BytecodeGenerator createContextualBytecodeGenerator;

    private boolean constructorGenerated;

    private int createContextualMaximumStackSize;
    private int createContextualMaximumLocalVariables;

    private Collection<BytecodeEvent> prepareContextualBytecodeEvents;
    private BytecodeGenerator prepareContextualBytecodeGenerator;

    private int prepareContextualMaximumStackSize;
    private int prepareContextualMaximumLocalVariables;

    private Collection<BytecodeEvent> prepareContextualFieldsBytecodeEvents;
    private BytecodeGenerator prepareContextualFieldsBytecodeGenerator;

    private int prepareContextualFieldsMaximumStackSize;
    private int prepareContextualFieldsMaximumLocalVariables;

    private Label prepareContextualFieldsTryBlockStartLabel;
    private Label prepareContextualFieldsTryBlockEndLabel;
    private Label prepareContextualFieldsCatchBlockStartLabel;
    private Label prepareContextualFieldsCatchBlockEndLabel;

    private Collection<BytecodeEvent> prepareContextualMethodsBytecodeEvents;
    private BytecodeGenerator prepareContextualMethodsBytecodeGenerator;

    private int prepareContextualMethodsMaximumStackSize;
    private int prepareContextualMethodsMaximumLocalVariables;

    private Label prepareContextualMethodsTryBlockStartLabel;
    private Label prepareContextualMethodsTryBlockEndLabel;
    private Label prepareContextualMethodsCatchBlockStartLabel;
    private Label prepareContextualMethodsCatchBlockEndLabel;

    private String className;
    private int classModifiers;

    private String contextualFactoryClassName;
    private String contextualFactorySimpleClassName;

    private String fieldName;
    private String fieldDescriptor;

    private String methodName;
    private String methodDescriptor;

    private boolean isConstructor;

    private boolean hasDefaultConstructor;

    public InjectionTransformation() {
    }

    @Override
    public boolean testRetain(final BytecodeContext aBytecodeContext, final BytecodeEvent aBytecodeEvent) {
        return aBytecodeEvent.getType() == BytecodeEvent.CLASS_START;
    }

    @Override
    public boolean testProcess(final BytecodeContext aBytecodeContext, final BytecodeEvent aBytecodeEvent) {
        return aBytecodeEvent.getType() == BytecodeEvent.ANNOTATION_START
            && aBytecodeEvent.asAnnotationStart().equalsType(Inject.class);
    }

    @Override
    public boolean testSkip(final BytecodeContext aBytecodeContext, final BytecodeEvent aBytecodeEvent) {
        return aBytecodeEvent.getType() == BytecodeEvent.CLASS_START && aBytecodeEvent.asClassStart().isInterface();
    }

    @Override
    public void handleBytecodeEvent(final BytecodeContext aBytecodeContext, final BytecodeEvent aBytecodeEvent) {
        switch (aBytecodeEvent.getType()) {
        case BytecodeEvent.CLASS_START:
            handleClassStartBytecodeEvent(aBytecodeContext, aBytecodeEvent.asClassStart());
            break;
        case BytecodeEvent.ANNOTATION_START:
            handleAnnotationStartBytecodeEvent(aBytecodeContext, aBytecodeEvent.asAnnotationStart());
            break;
        case BytecodeEvent.FIELD_START:
            handleFieldStartBytecodeEvent(aBytecodeContext, aBytecodeEvent.asFieldStart());
            break;
        case BytecodeEvent.FIELD_END:
            handleFieldEndBytecodeEvent(aBytecodeContext, aBytecodeEvent.asFieldEnd());
            break;
        case BytecodeEvent.METHOD_START:
            handleMethodStartBytecodeEvent(aBytecodeContext, aBytecodeEvent.asMethodStart());
            break;
        case BytecodeEvent.METHOD_END:
            handleMethodEndBytecodeEvent(aBytecodeContext, aBytecodeEvent.asMethodEnd());
            break;
        case BytecodeEvent.CLASS_END:
            handleClassEndBytecodeEvent(aBytecodeContext, aBytecodeEvent.asClassEnd());
            break;
        default:
            break;
        }

        aBytecodeContext.forwardBytecodeEvent(aBytecodeEvent);
    }

    private void handleClassStartBytecodeEvent(final BytecodeContext aBytecodeContext,
        final ClassStartBytecodeEvent aClassStartBytecodeEvent)
    {
        className = aClassStartBytecodeEvent.getName();
        classModifiers = aClassStartBytecodeEvent.getModifiers();

        contextualFactoryClassName = className + "$InjectionFactory";
        contextualFactorySimpleClassName = contextualFactoryClassName.substring(contextualFactoryClassName
            .lastIndexOf('/') + 1);

        prepareContextualFactory();
    }

    private void handleAnnotationStartBytecodeEvent(final BytecodeContext aBytecodeContext,
        final AnnotationStartBytecodeEvent aBytecodeEvent)
    {
        if (!aBytecodeEvent.equalsType(Inject.class)) {
            return;
        }

        extendPrepareContextualFields();
        extendCreateContextual();
        extendPrepareContextualMethods();
    }

    private void handleFieldStartBytecodeEvent(final BytecodeContext aBytecodeContext,
        final FieldStartBytecodeEvent aBytecodeEvent)
    {
        fieldName = aBytecodeEvent.getName();
        fieldDescriptor = aBytecodeEvent.getDescriptor();
    }

    private void handleFieldEndBytecodeEvent(final BytecodeContext aBytecodeContext,
        final FieldEndBytecodeEvent aBytecodeEvent)
    {
        fieldName = null;
    }

    private void handleMethodStartBytecodeEvent(final BytecodeContext aBytecodeContext,
        final MethodStartBytecodeEvent aBytecodeEvent)
    {
        methodName = aBytecodeEvent.getName();
        methodDescriptor = aBytecodeEvent.getDescriptor();
        isConstructor = aBytecodeEvent.isConstructor();

        if (isConstructor && "()V".equals(methodDescriptor)) {
            hasDefaultConstructor = true;
        }
    }

    private void handleMethodEndBytecodeEvent(final BytecodeContext aBytecodeContext,
        final MethodEndBytecodeEvent aBytecodeEvent)
    {
        methodName = null;
    }

    private void handleClassEndBytecodeEvent(final BytecodeContext aBytecodeContext,
        final ClassEndBytecodeEvent aBytecodeEvent)
    {
        aBytecodeContext.getBytecodeGenerator().referenceInnerClass(contextualFactoryClassName, className,
            contextualFactorySimpleClassName, Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC);

        finishContextualFactory();
    }

    private void prepareContextualFactory() {
        contextualFactoryBytecodeGenerator = Bytecode.createGenerator(Bytecode.createWriter());
        contextualFactoryBytecodeGenerator.startClass(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC,
            contextualFactoryClassName, Object.class, Contextual.class, ContextualFactory.class);

        implementContextField();
        implementConstructor();
        implementGetNames();
        implementGetValue();
        implementGetScope();
        implementOnBound();
        implementOnUnbound();
        implementGetContextualClass();

        prepareCreateContextual();
        preparePrepareContextual();
        preparePrepareContextualFields();
        preparePrepareContextualMethods();
    }

    private void implementContextField() {
        contextualFactoryBytecodeGenerator.startField(Opcodes.ACC_PRIVATE, "context", Context.class);
        contextualFactoryBytecodeGenerator.endField();
    }

    private void implementConstructor() {
        contextualFactoryBytecodeGenerator.startMethod(Opcodes.ACC_PUBLIC, "<init>", "()V");
        contextualFactoryBytecodeGenerator.startCode();

        contextualFactoryBytecodeGenerator.loadReference(0);
        contextualFactoryBytecodeGenerator.invokeSpecial(Object.class, "<init>", void.class);
        contextualFactoryBytecodeGenerator.returnVoid();

        contextualFactoryBytecodeGenerator.endCode(1, 1);
        contextualFactoryBytecodeGenerator.endMethod();
    }

    private void implementGetNames() {
        contextualFactoryBytecodeGenerator.startMethod(Opcodes.ACC_PUBLIC, "getNames", String[].class);
        contextualFactoryBytecodeGenerator.startCode();

        contextualFactoryBytecodeGenerator.pushInteger(1);
        contextualFactoryBytecodeGenerator.newReferenceArray(String.class);

        contextualFactoryBytecodeGenerator.duplicateReference();
        contextualFactoryBytecodeGenerator.pushInteger(0);
        contextualFactoryBytecodeGenerator.pushConstant(contextualFactoryClassName.replace('/', '.'));
        contextualFactoryBytecodeGenerator.storeReferenceArray();

        contextualFactoryBytecodeGenerator.returnReference();

        contextualFactoryBytecodeGenerator.endCode(4, 1);
        contextualFactoryBytecodeGenerator.endMethod();
    }

    private void implementGetValue() {
        contextualFactoryBytecodeGenerator.startMethod(Opcodes.ACC_PUBLIC, "getValue", Object.class);
        contextualFactoryBytecodeGenerator.startCode();

        contextualFactoryBytecodeGenerator.loadReference(0);
        contextualFactoryBytecodeGenerator.returnReference();

        contextualFactoryBytecodeGenerator.endCode(1, 1);
        contextualFactoryBytecodeGenerator.endMethod();
    }

    private void implementGetScope() {
        contextualFactoryBytecodeGenerator.startMethod(Opcodes.ACC_PUBLIC, "getScope", Class.class);
        contextualFactoryBytecodeGenerator.startCode();

        contextualFactoryBytecodeGenerator.pushConstant(DynamicScoped.class);
        contextualFactoryBytecodeGenerator.returnReference();

        contextualFactoryBytecodeGenerator.endCode(1, 1);
        contextualFactoryBytecodeGenerator.endMethod();
    }

    private void implementOnBound() {
        contextualFactoryBytecodeGenerator.startMethod(Opcodes.ACC_PUBLIC, "onBound", void.class, Context.class);
        contextualFactoryBytecodeGenerator.startCode();

        contextualFactoryBytecodeGenerator.loadReference(0);
        contextualFactoryBytecodeGenerator.loadReference(1);
        contextualFactoryBytecodeGenerator.storeField(contextualFactoryClassName, "context", Context.class);
        contextualFactoryBytecodeGenerator.returnVoid();

        contextualFactoryBytecodeGenerator.endCode(2, 1);
        contextualFactoryBytecodeGenerator.endMethod();
    }

    private void implementOnUnbound() {
        contextualFactoryBytecodeGenerator.startMethod(Opcodes.ACC_PUBLIC, "onUnbound", void.class, Context.class);
        contextualFactoryBytecodeGenerator.startCode();

        contextualFactoryBytecodeGenerator.loadReference(0);
        contextualFactoryBytecodeGenerator.pushNull();
        contextualFactoryBytecodeGenerator.storeField(contextualFactoryClassName, "context", Context.class);
        contextualFactoryBytecodeGenerator.returnVoid();

        contextualFactoryBytecodeGenerator.endCode(2, 1);
        contextualFactoryBytecodeGenerator.endMethod();
    }

    private void implementGetContextualClass() {
        contextualFactoryBytecodeGenerator.startMethod(Opcodes.ACC_PUBLIC, "getContextualClass", Class.class);
        contextualFactoryBytecodeGenerator.startCode();

        contextualFactoryBytecodeGenerator.pushConstant(Type.getType(String.format("L%s;", className)));
        contextualFactoryBytecodeGenerator.returnReference();

        contextualFactoryBytecodeGenerator.endCode(1, 1);
        contextualFactoryBytecodeGenerator.endMethod();
    }

    private void prepareCreateContextual() {
        createContextualBytecodeEvents = new ArrayList<>();
        createContextualBytecodeGenerator = Bytecode.createGenerator(createContextualBytecodeEvents);

        createContextualBytecodeGenerator.startMethod(Opcodes.ACC_PUBLIC, "createContextual", Object.class);
        createContextualBytecodeGenerator.startCode();

        // <<1>> = (InjectionService) this.context.lookup(InjectionService.class);
        createContextualBytecodeGenerator.loadReference(0);
        createContextualBytecodeGenerator.loadField(contextualFactoryClassName, "context", Context.class);
        createContextualBytecodeGenerator.pushConstant(InjectionService.class);
        createContextualBytecodeGenerator.invokeInterface(Context.class, "lookup", Object.class, Class.class);
        createContextualBytecodeGenerator.castChecked(InjectionService.class);
        createContextualBytecodeGenerator.storeReference(1);
        // stack = 2; locals = 2

        createContextualMaximumStackSize = 2;
        createContextualMaximumLocalVariables = 2;
    }

    private void extendCreateContextual() {
        if (methodName == null || !isConstructor) {
            return;
        }

        if (constructorGenerated) {
            throw new BytecodeException(String.format("Class '%s' may only declare one @Inject-annotated constructor.",
                className.replace('/', '.')));
        }

        constructorGenerated = true;

        if ((classModifiers & Opcodes.ACC_ABSTRACT) == Opcodes.ACC_ABSTRACT) {
            createContextualBytecodeGenerator.newReference(InjectionException.class);
            createContextualBytecodeGenerator.duplicateReference();
            createContextualBytecodeGenerator.pushConstant(String.format("Cannot instantiate abstract class '%s'",
                className.replace('/', '.')));
            createContextualBytecodeGenerator.invokeConstructor(InjectionException.class, String.class);
            createContextualBytecodeGenerator.throwReference();

            createContextualMaximumStackSize = Math.max(createContextualMaximumStackSize, 3);
        } else {
            final Type[] argumentTypes = Type.getArgumentTypes(methodDescriptor);

            if (argumentTypes.length > 0) {
                // Generates:
                // <<2>> = <<className>>.class.getDeclaredMethod(<<methodName>>,
                // <<parameterType1>>, ...,
                // <<parameterTypeN>>).getParameterAnnotations()
                createContextualBytecodeGenerator.pushConstant(Type.getObjectType(className));
                createContextualBytecodeGenerator.pushInteger(argumentTypes.length);
                createContextualBytecodeGenerator.newReferenceArray(Class.class);

                for (int i = 0; i < argumentTypes.length; i++) {
                    final Type argumentType = argumentTypes[i];
                    createContextualBytecodeGenerator.duplicateReference();
                    createContextualBytecodeGenerator.pushInteger(i);
                    createContextualBytecodeGenerator.pushConstant(argumentType);
                    createContextualBytecodeGenerator.storeReferenceArray();
                }

                createContextualBytecodeGenerator.invokeVirtual(Class.class, "getDeclaredConstructor",
                    Constructor.class, Class[].class);
                createContextualBytecodeGenerator.invokeVirtual(Method.class, "getParameterAnnotations",
                    Annotation[][].class);
                createContextualBytecodeGenerator.storeReference(2);

                // maxstack = 5; locals = 3 if argumentTypes.length != 0; curstack = 0
            }

            // Generates:
            // <<2>> = new <<className>>(
            // <<1>>.injectValue(null, null, <<argumentType1>>, <<2>>[1]),
            // ...,
            // <<1>>.injectValue(null, null, <<argumentTypeN>>, <<2>>[N])
            // )

            createContextualBytecodeGenerator.newReference(className);
            createContextualBytecodeGenerator.duplicateReference();
            // curstack = 2

            for (int i = 0; i < argumentTypes.length; i++) {
                final Type argumentType = argumentTypes[i];

                createContextualBytecodeGenerator.loadReference(1);

                createContextualBytecodeGenerator.pushNull();
                // TODO Don't push null, but the parameter name if available (Java 8 required)
                createContextualBytecodeGenerator.pushNull();
                createContextualBytecodeGenerator.pushConstant(argumentType);
                createContextualBytecodeGenerator.loadReference(2);
                createContextualBytecodeGenerator.loadReferenceArray(i);

                createContextualBytecodeGenerator.invokeInterface(InjectionService.class, "injectValue", Object.class,
                    String.class, Object.class, Class.class, Annotation[].class);
                createContextualBytecodeGenerator.castChecked(argumentType);

                // curstack = curstack' + 1; maxstack = curstack' + 6
            }

            createContextualBytecodeGenerator.invokeConstructor(className, methodDescriptor);
            createContextualBytecodeGenerator.storeReference(2);
            // curstack = curstack' - 1; locals = 3 if argumentTypes.length == 0

            if (argumentTypes.length == 0) {
                createContextualMaximumStackSize = Math.max(createContextualMaximumStackSize, 2);
                createContextualMaximumLocalVariables = Math.max(createContextualMaximumLocalVariables, 3);
            } else {
                createContextualMaximumStackSize = Math.max(createContextualMaximumStackSize, 8 + argumentTypes.length);
                createContextualMaximumLocalVariables = Math.max(createContextualMaximumLocalVariables, 3);
            }
        }
    }

    private void finishCreateContextual() {
        if (!constructorGenerated) {
            if (!hasDefaultConstructor) {
                throw new BytecodeException(String.format(
                    "Class '%s' must either define the parameterless default constructor or "
                        + "one @Inject-annotated constructor.", className.replace('/', '.')));
            }

            if ((classModifiers & Opcodes.ACC_ABSTRACT) == Opcodes.ACC_ABSTRACT) {
                createContextualBytecodeGenerator.newReference(InjectionException.class);
                createContextualBytecodeGenerator.duplicateReference();
                createContextualBytecodeGenerator.pushConstant(String.format("Cannot instantiate abstract class '%s'",
                    className.replace('/', '.')));
                createContextualBytecodeGenerator.invokeConstructor(InjectionException.class, String.class);
                createContextualBytecodeGenerator.throwReference();
            } else {
                createContextualBytecodeGenerator.newReference(className);
                createContextualBytecodeGenerator.duplicateReference();
                createContextualBytecodeGenerator.invokeConstructor(className, void.class);
                createContextualBytecodeGenerator.storeReference(2);
            }
        }

        createContextualBytecodeGenerator.loadReference(2);
        createContextualBytecodeGenerator.returnReference();

        createContextualBytecodeGenerator.endCode(createContextualMaximumStackSize,
            createContextualMaximumLocalVariables);
        createContextualBytecodeGenerator.endMethod();
    }

    private void preparePrepareContextual() {
        prepareContextualBytecodeEvents = new ArrayList<>();
        prepareContextualBytecodeGenerator = Bytecode.createGenerator(prepareContextualBytecodeEvents);

        prepareContextualBytecodeGenerator.startMethod(Opcodes.ACC_PUBLIC, "prepareContextual", void.class,
            Object.class);
        prepareContextualBytecodeGenerator.startCode();

        // <<2>> = (InjectionService) this.context.lookup(InjectionService.class);
        prepareContextualBytecodeGenerator.loadReference(0);
        prepareContextualBytecodeGenerator.loadField(contextualFactoryClassName, "context", Context.class);
        prepareContextualBytecodeGenerator.pushConstant(InjectionService.class);
        prepareContextualBytecodeGenerator.invokeInterface(Context.class, "lookup", Object.class, Class.class);
        prepareContextualBytecodeGenerator.castChecked(InjectionService.class);
        prepareContextualBytecodeGenerator.storeReference(2);

        prepareContextualMaximumStackSize = 2;
        prepareContextualMaximumLocalVariables = 3;
    }

    private void preparePrepareContextualFields() {
        prepareContextualFieldsBytecodeEvents = new ArrayList<>();
        prepareContextualFieldsBytecodeGenerator = Bytecode.createGenerator(prepareContextualFieldsBytecodeEvents);

        prepareContextualFieldsMaximumStackSize = 3;
        prepareContextualFieldsMaximumLocalVariables = 3;

        prepareContextualFieldsTryBlockStartLabel = new Label();
        prepareContextualFieldsTryBlockEndLabel = new Label();
        prepareContextualFieldsCatchBlockStartLabel = new Label();
        prepareContextualFieldsCatchBlockEndLabel = new Label();

        prepareContextualFieldsBytecodeGenerator.markTryCatchBlock(prepareContextualFieldsTryBlockStartLabel,
            prepareContextualFieldsTryBlockEndLabel, prepareContextualFieldsCatchBlockStartLabel,
            "java/lang/NoSuchFieldException");
        prepareContextualFieldsBytecodeGenerator.markLabel(prepareContextualFieldsTryBlockStartLabel);

        prepareContextualFieldsBytecodeGenerator.doNothing();
    }

    private void extendPrepareContextualFields() {
        if (fieldName == null) {
            return;
        }

        final Type fieldType = Type.getType(fieldDescriptor);

        // Generates: <<1>>.<<fieldName>> = (<<fieldType>>)
        // <<2>>.injectValue(<<1>>, <<fieldName>>, <<fieldType>>,
        // <<className>>.class.getDeclaredField(<<fieldName>>).getAnnotations());

        prepareContextualFieldsBytecodeGenerator.loadReference(1);

        prepareContextualFieldsBytecodeGenerator.loadReference(2);

        prepareContextualFieldsBytecodeGenerator.loadReference(1); // aComponent
        prepareContextualFieldsBytecodeGenerator.pushConstant(fieldName);
        prepareContextualFieldsBytecodeGenerator.pushConstant(fieldType); // aTargetType

        // Generates:
        // <<className>>.getDeclaredField(<<fieldName>>).getAnnotations()
        prepareContextualFieldsBytecodeGenerator.pushConstant(Type.getObjectType(className));
        prepareContextualFieldsBytecodeGenerator.pushConstant(fieldName);
        prepareContextualFieldsBytecodeGenerator.invokeVirtual(Class.class, "getDeclaredField", Field.class,
            String.class);
        prepareContextualFieldsBytecodeGenerator.invokeVirtual(Field.class, "getAnnotations", Annotation[].class); // aTargetAnnotations

        prepareContextualFieldsBytecodeGenerator.invokeInterface(InjectionService.class, "injectValue", Object.class,
            String.class, Object.class, Class.class, Annotation[].class);
        prepareContextualFieldsBytecodeGenerator.castChecked(fieldType);

        prepareContextualFieldsBytecodeGenerator.storeField(className, fieldName, fieldDescriptor);

        prepareContextualFieldsMaximumStackSize = Math.max(prepareContextualFieldsMaximumStackSize, 7);
        prepareContextualFieldsMaximumLocalVariables = Math.max(prepareContextualFieldsMaximumLocalVariables, 3);
    }

    private void finishPrepareContextualFields() {
        prepareContextualFieldsBytecodeGenerator.markLabel(prepareContextualFieldsTryBlockEndLabel);
        prepareContextualFieldsBytecodeGenerator.gotoLabel(prepareContextualFieldsCatchBlockEndLabel);

        prepareContextualFieldsBytecodeGenerator.markLabel(prepareContextualFieldsCatchBlockStartLabel);
        prepareContextualFieldsBytecodeGenerator.markFrameSame1(0, null, 1, new Object[] {
            "java/lang/NoSuchFieldException"
        });

        prepareContextualFieldsBytecodeGenerator.storeReference(2);
        prepareContextualFieldsBytecodeGenerator.newReference(InjectionException.class);
        prepareContextualFieldsBytecodeGenerator.duplicateReference();
        prepareContextualFieldsBytecodeGenerator.loadReference(2);
        prepareContextualFieldsBytecodeGenerator.invokeConstructor(InjectionException.class, Throwable.class);
        prepareContextualFieldsBytecodeGenerator.throwReference();

        prepareContextualFieldsBytecodeGenerator.markLabel(prepareContextualFieldsCatchBlockEndLabel);
        prepareContextualFieldsBytecodeGenerator.markFrameSame();
    }

    private void preparePrepareContextualMethods() {
        prepareContextualMethodsBytecodeEvents = new ArrayList<>();
        prepareContextualMethodsBytecodeGenerator = Bytecode.createGenerator(prepareContextualMethodsBytecodeEvents);

        prepareContextualMethodsMaximumStackSize = 3;
        prepareContextualMethodsMaximumLocalVariables = 3;

        prepareContextualMethodsTryBlockStartLabel = new Label();
        prepareContextualMethodsTryBlockEndLabel = new Label();
        prepareContextualMethodsCatchBlockStartLabel = new Label();
        prepareContextualMethodsCatchBlockEndLabel = new Label();

        prepareContextualMethodsBytecodeGenerator.markTryCatchBlock(prepareContextualMethodsTryBlockStartLabel,
            prepareContextualMethodsTryBlockEndLabel, prepareContextualMethodsCatchBlockStartLabel,
            "java/lang/NoSuchMethodException");
        prepareContextualMethodsBytecodeGenerator.markLabel(prepareContextualMethodsTryBlockStartLabel);

        prepareContextualMethodsBytecodeGenerator.doNothing();
    }

    private void extendPrepareContextualMethods() {
        if (methodName == null || isConstructor) {
            return;
        }

        final Type[] argumentTypes = Type.getArgumentTypes(methodDescriptor);

        if (argumentTypes.length > 0) {
            // Generates:
            // <<3>> = <<className>>.class.getDeclaredMethod(<<methodName>>,
            // <<parameterType1>>, ...,
            // <<parameterTypeN>>).getParameterAnnotations()
            prepareContextualMethodsBytecodeGenerator.pushConstant(Type.getObjectType(className));
            prepareContextualMethodsBytecodeGenerator.pushConstant(methodName);
            prepareContextualMethodsBytecodeGenerator.pushInteger(argumentTypes.length);
            prepareContextualMethodsBytecodeGenerator.newReferenceArray(Class.class);
            // curstack = 4

            for (int i = 0; i < argumentTypes.length; i++) {
                final Type argumentType = argumentTypes[i];
                prepareContextualMethodsBytecodeGenerator.duplicateReference();
                prepareContextualMethodsBytecodeGenerator.pushInteger(i);
                prepareContextualMethodsBytecodeGenerator.pushConstant(argumentType);
                prepareContextualMethodsBytecodeGenerator.storeReferenceArray();
            }

            prepareContextualMethodsBytecodeGenerator.invokeVirtual(Class.class, "getDeclaredMethod", Method.class,
                String.class, Class[].class);
            prepareContextualMethodsBytecodeGenerator.invokeVirtual(Method.class, "getParameterAnnotations",
                Annotation[][].class);
            prepareContextualMethodsBytecodeGenerator.storeReference(3);

            // maxstack = 7; maxlocals = 4;
        }

        // Generates:
        // <<1>>.<<methodName>>(
        // <<2>>.injectValue(this, <<parameterType>>, <<3>>[1]),
        // ...,
        // <<2>>.injectValue(this, <<parameterType>>, <<3>>[N])
        // )

        prepareContextualMethodsBytecodeGenerator.loadReference(1);

        for (int i = 0; i < argumentTypes.length; i++) {
            final Type argumentType = argumentTypes[i];

            prepareContextualMethodsBytecodeGenerator.loadReference(2);

            prepareContextualMethodsBytecodeGenerator.loadReference(1);
            // TODO Don't push null, but the parameter name if available (Java 8 required)
            prepareContextualMethodsBytecodeGenerator.pushNull();
            prepareContextualMethodsBytecodeGenerator.pushConstant(argumentType);
            prepareContextualMethodsBytecodeGenerator.loadReference(3);
            prepareContextualMethodsBytecodeGenerator.loadReferenceArray(i);

            prepareContextualMethodsBytecodeGenerator.invokeInterface(InjectionService.class, "injectValue",
                Object.class, String.class, Object.class, Class.class, Annotation[].class);
            prepareContextualMethodsBytecodeGenerator.castChecked(argumentType);
        }

        prepareContextualMethodsBytecodeGenerator.invokeVirtual(className, methodName, methodDescriptor);
        if (!methodDescriptor.endsWith(")V")) {
            prepareContextualMethodsBytecodeGenerator.popReference();
        }

        if (argumentTypes.length == 0) {
            prepareContextualMethodsMaximumStackSize = Math.max(prepareContextualMethodsMaximumStackSize, 1);
        } else {
            prepareContextualMethodsMaximumStackSize = Math.max(prepareContextualMethodsMaximumStackSize,
                7 + argumentTypes.length);
            prepareContextualMethodsMaximumLocalVariables = Math.max(prepareContextualMethodsMaximumLocalVariables, 4);
        }
    }

    private void finishPrepareContextualMethods() {
        prepareContextualMethodsBytecodeGenerator.markLabel(prepareContextualMethodsTryBlockEndLabel);
        prepareContextualMethodsBytecodeGenerator.gotoLabel(prepareContextualMethodsCatchBlockEndLabel);

        prepareContextualMethodsBytecodeGenerator.markLabel(prepareContextualMethodsCatchBlockStartLabel);
        prepareContextualMethodsBytecodeGenerator.markFrameSame1(0, null, 1, new Object[] {
            "java/lang/NoSuchMethodException"
        });

        prepareContextualMethodsBytecodeGenerator.storeReference(2);
        prepareContextualMethodsBytecodeGenerator.newReference(InjectionException.class);
        prepareContextualMethodsBytecodeGenerator.duplicateReference();
        prepareContextualMethodsBytecodeGenerator.loadReference(2);
        prepareContextualMethodsBytecodeGenerator.invokeConstructor(InjectionException.class, Throwable.class);
        prepareContextualMethodsBytecodeGenerator.throwReference();

        prepareContextualMethodsBytecodeGenerator.markLabel(prepareContextualMethodsCatchBlockEndLabel);
        prepareContextualMethodsBytecodeGenerator.markFrameSame();
    }

    private void finishPrepareContextual() {
        prepareContextualBytecodeGenerator.dispatchBytecodeEvents(prepareContextualFieldsBytecodeEvents);
        prepareContextualBytecodeGenerator.dispatchBytecodeEvents(prepareContextualMethodsBytecodeEvents);

        prepareContextualBytecodeGenerator.returnVoid();

        prepareContextualBytecodeGenerator.endCode(
            Math.max(prepareContextualMaximumStackSize,
                Math.max(prepareContextualFieldsMaximumStackSize, prepareContextualMethodsMaximumStackSize)),
            Math.max(prepareContextualMaximumLocalVariables,
                Math.max(prepareContextualFieldsMaximumLocalVariables, prepareContextualMethodsMaximumLocalVariables)));
        prepareContextualBytecodeGenerator.endMethod();
    }

    private void finishContextualFactory() {
        finishCreateContextual();
        finishPrepareContextualFields();
        finishPrepareContextualMethods();
        finishPrepareContextual();

        contextualFactoryBytecodeGenerator.dispatchBytecodeEvents(createContextualBytecodeEvents);
        contextualFactoryBytecodeGenerator.dispatchBytecodeEvents(prepareContextualBytecodeEvents);

        contextualFactoryBytecodeGenerator.endClass();
    }
}
